/*
 * Team Name: Team Sasquatch
 * Cody Sharff
 * Anthony Rios
 * Prince Kishore
 * Travis Drake
 */

package model;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Class MasterCollection is a container for Auction's. 
 * 
 * @author Cody Sharff
 * @author Modified by Travis Drake: modified and added to the accessors and setters for auctions and items.
 */
public class MasterCollection {
    
	/**
	 * A map that contains user accounts for bidders,
	 * AuctionCentral employees and NPO's.
	 */
    private final Map<String, User> my_users;
	
    /**
     * A map that contains all the auction's currently scheduled.
     */
	private final Map<Integer, Auction> my_auctions;
	
	/**
	 * The maximum number of auction's that can be scheduled in the
	 * future.
	 */
	public final static int MAX_AUCTIONS = 25;
	
	/**
	 * Determines how many months ahead that an auction can
	 * be scheduled.
	 */
	public final static int MAX_MONTHS_AHEAD = 3;
	
	/**
	 * Constructs a MasterCollection object that initializes
	 * the user and auction hashmaps.
	 */
	public MasterCollection() {
	    my_users = new HashMap<String, User>();
		my_auctions = new HashMap<Integer, Auction>();                            
	}

	/**
	 * Adds an auction to the collection of auction's.
	 * 
	 * @requires the_auction_id >= 1
	 * @requires the_npo != null
	 * @requires the_contact_person != null
	 * @requires the_intake_person != null || the_intake_person == null
	 * @requires the_auction_date != null
	 * @ensures parsed Auction data, passed to Auction constructor
	 * 
	 * 
	 */
	public void addAuction(int the_auction_id, String the_npo, String the_contact_person, long contactNumber,
			String the_intake_person, Calendar the_auction_date, int the_auction_duration,
			String the_comments) {
		my_auctions.put(the_auction_id, new Auction(the_auction_id, the_npo, the_contact_person,
				contactNumber, the_intake_person, the_auction_date, the_auction_duration,
				the_comments)); //parse the_info and pass into Auction constructor
	}
	
	/**
	 * Adds an already created auctions to the master collections
	 * @param the_auction
	 */
	public void addAuction(Auction the_auction) {
		my_auctions.put(the_auction.getAuctionID(), the_auction);
	}

	/**
	 * Adds an item to an auction.
	 * 
	 * @requires the_auction_id >= 1
	 * @requires the_item_name !+ null
	 * @requires the_quantity > 0
	 * @requires the_min_start_bid >= 1.00
	 * @requires the_storage != null
	 * @requires the_condition != null
	 * @ensures my_auction.get(the_auction_id) != null
	 * @ensures my_auction.size == my_auction.size \old + 1
	 * 
	 */
	public void addAuctionItem(int the_auction_id, String the_item_name, 
			int the_quantity, int the_min_start_bid, String the_donor, int the_size,
			String the_storage, Condition the_condition, String the_comments) {
		my_auctions.get(the_auction_id).addItem(the_item_name, the_quantity, the_min_start_bid, the_donor, the_size,
				the_storage, the_condition, the_comments);
	}

	/**
	 * Adds an item to an auction.
	 * 
	 */
	public void addAuctionItem(int the_auction_id, Item the_item) {
	    my_auctions.get(the_auction_id).addItem(the_item);
	}
	
	/**
	 * Given an auction id this method will return the
	 * auction object.
	 * 
	 */
	public Auction getAuction(int the_auction_id) {
		return my_auctions.get(the_auction_id);
		
	}
    
    /**
     * Returns a list of the auction's currently pending, sorted by auction
     * ID number.
     */
    public List<Auction> getAuctions() {
        List<Auction> list = new ArrayList<Auction>(my_auctions.values());
        Collections.sort(list, Auction.ID_ASC);
        return list;
    }
    
    /**
     * Returns a list of the Users registered in the system in no particular
     * order.
     * 
     * @author Travis Drake
     */
    public List<User> getUsers() {
        List<User> list = new ArrayList<User>(my_users.values());
        return list;
    }
	
    /**
     * Can return a list of all the auctions based on a certain
     * sorting order defined by a enum.
     */
	public List<Auction> getAuctions(SortType the_sort_type) {
		List<Auction> list = new ArrayList<Auction>(my_auctions.values());
		switch (the_sort_type) {
			case DATE:
				Collections.sort(list, Auction.DATE_ASC);
				break;
			case ID:
				Collections.sort(list, Auction.ID_ASC);
				break;
			case NUM_ITEMS:
				Collections.sort(list, Auction.NUM_ITEMS_ASC);
				break;
			case START_TIME:
				Collections.sort(list, Auction.START_TIME_ASC);
				break;
			case DURATION:
				Collections.sort(list, Auction.DURATION_ASC);
				break;
		}
		return list;
	}
	
	/**
	 * Given an Auction ID, this me
	 * @param the_auction_id
	 * @param the_sort_type
	 * @return
	 */
	public List<Item> getAuctionItems(int the_auction_id, SortType the_sort_type) {
		return my_auctions.get(the_auction_id).getItems();
	}
	
	/**
	 * Adds/registers a user into the system
	 * 
	 * @requires the_name != null
	 * @requires the_user_type == (UserType.NPO || UserType.AUCTION_CENTRAL || UserType.BIDDER)
	 * @requires the_password != null
	 * @ensures my_users.contains(the_name)
	 */
	public boolean addUser(String the_name, UserType the_user_type, String the_password) {
	    boolean added = false;
	    if (!my_users.containsKey(the_name)) {
	        my_users.put(the_name, new User(the_name, the_user_type, the_password));
	        added = true;
	    }
	    return added;
	}

    /**
     * Determines if the user name is valid.
     */
    public boolean containsUser(String the_name) {
        return my_users.containsKey(the_name);
    }

    /**
     * Given a string representing a username, the method
     * returns a user object.
     */
    public User getUser(String the_login) {
        return my_users.get(the_login);
    }
	
    /**
     * Given an auction id, this method removes the auction
     * matching the auction id.
     * 
     * @requires the_auction_id > 0
     * @ensures !(my_auctions.contains(the_auction_id))
     */
	public void removeAuction(int the_auction_id) {
		my_auctions.remove(the_auction_id);
	}
	
	/**
	 * Checks if adding an auction is valid given certain
	 * restrictions.
	 * 
	 * @requires the_auction != null
	 * @ensures valid date
	 */
	public List<ErrorType> checkValid(Auction the_auction) {
		List<ErrorType> error = new ArrayList<ErrorType>();
		Calendar current_date = new GregorianCalendar();
		List<Auction> auctions_on_date = getAuctionsOnDate(the_auction.getDate());
		if (auctions_on_date.size() == 1) {
			//Add auction duration to the start time of the auction
			//Add two hours to the end of the first auction
		    Calendar temp_reference = auctions_on_date.get(0).getDate();
			Calendar two_hours_after_end = (Calendar) temp_reference.clone();
			two_hours_after_end.add(Calendar.HOUR_OF_DAY, the_auction.getDuration() / 60 + 2);
			if (the_auction.getDate().before(two_hours_after_end)) {
				error.add(ErrorType.TOO_SOON_TO_END_OF_PREVIOUS_AUCTION);
			}
		}
		
		//No more than one auction per year per Non-profit organization can be scheduled.
		Calendar cur_year_begin = new GregorianCalendar();
		cur_year_begin.set(current_date.get(current_date.YEAR), 0, 1);
		Calendar cur_year_end = new GregorianCalendar();
		cur_year_end.set(current_date.get(current_date.YEAR), 11, 31);
		List<Auction> auctions_in_year = getAuctionsBetweenDate(cur_year_begin, cur_year_end);
		for (Auction auction : auctions_in_year) {
		    if(auction.getNPO().compareTo(the_auction.getNPO()) == 0) {
		    	error.add(ErrorType.MORE_THAN_ONE_AUCTION_PER_YEAR);
		    }
		}
		
		error.addAll(checkDateValid(the_auction.getDate()));
		
		if (error.isEmpty()) {
			error.add(ErrorType.NO_ERROR); 			
		}

		return error;
	}
	
	/**
	 * Given a date and time, checks to see if adding an auction on
	 * that date would be valid.
	 * 
	 */
	public List<ErrorType> checkDateValid(Calendar the_auction_date) {
		List<ErrorType> error = new ArrayList<ErrorType>();
		Calendar three_months = Calendar.getInstance();
		three_months.add(Calendar.MONTH, +3);
		
		// Is adding the auction going to cause there to be
		// more than 25 auctions in the future?
		if (getAuctionsFromCurDate().size() >= MAX_AUCTIONS) {
			error.add(ErrorType.MORE_THAN_25_AUCTIONS);
//            System.out.println("25 auction limit error");
		}
		
		// An auction may not be scheduled more than 3 months from
		// the current date.
		if (the_auction_date.after(three_months)) {
			error.add(ErrorType.FARTHER_THAN_3_MONTHS);
//            System.out.println("3 months ahead error");
		}
		
		// No more than 5 auctions may be scheduled for any 7 day period
		Calendar three_days_before = new GregorianCalendar(the_auction_date.get(Calendar.YEAR), the_auction_date.get(Calendar.MONTH), the_auction_date.get(Calendar.DAY_OF_MONTH));
		three_days_before.add(Calendar.DAY_OF_MONTH, -3);
		Calendar three_days_after = new GregorianCalendar(the_auction_date.get(Calendar.YEAR), the_auction_date.get(Calendar.MONTH), the_auction_date.get(Calendar.DAY_OF_MONTH));
		three_days_after.add(Calendar.DAY_OF_MONTH, +4);
//		System.out.println(getAuctionsBetweenDate(three_days_before, three_days_after));
		if (getAuctionsBetweenDate(three_days_before, three_days_after).size() >= 5) {
			error.add(ErrorType.MORE_THAN_5_AUCTIONS_IN_7_DAYS);
//            System.out.println("5 in 7 days error");
		}
		
		// No more than 2 auctions can be scheduled on the same day, and
		// the start time of the second can be no earlier than 2 hours after the end time of the first.
		
        if (getAuctionsOnDate(the_auction_date).size() >= 2 ) {
            error.add(ErrorType.MORE_THAN_2_AUCTIONS_IN_A_DAY);
//            System.out.println("2 auctions in a day error");
                       
        }
        final Calendar temp_cal = Calendar.getInstance();
        // An auction may not be scheduled on a date that has already passed.
        if (the_auction_date.before(temp_cal)) {
            error.add(ErrorType.BEFORE_CURRENT_DATE);
//            System.out.println("Cannot schedule an auction in the past error");
        }
		return error;
	}
	
	/**
	 * Returns all the auctions into the future from the current
	 * date.
	 */
	public List<Auction> getAuctionsFromCurDate() {
		List<Auction> auctions = new ArrayList<Auction>();
		
		for (Auction auction : my_auctions.values()) {
		    if (auction.getDate().after(new GregorianCalendar())) {
		    	auctions.add(auction);
		    }
		}
		return auctions;
	}
	
	/**
	 * Given a start date and an end date, this method returns all the auctions
	 * in list from between the two dates.
	 */
	public List<Auction> getAuctionsBetweenDate(Calendar the_beginning_date,
		Calendar the_end_date) {
		List<Auction> auctions = new ArrayList<Auction>();
		
		for (Auction auction : my_auctions.values()) {
		    if (auction.getDate().after(the_beginning_date) && auction.getDate().before(the_end_date)) {
		    	auctions.add(auction);
		    }
		}
		return auctions;
	}
	
	/**
	 * Returns the amount of auctions on a given date.
	 */
	public List<Auction> getAuctionsOnDate(Calendar the_date) {
		List<Auction> auctions = new ArrayList<Auction>();
		for (Auction auction : my_auctions.values()) {
			if (TimeIgnoringComparator.compare(auction.getDate(), the_date) == 0) {
				auctions.add(auction);
			}
		}
		return auctions;
	}
	
	/**
	 * Comparator that compares Calendar objects while ignoring time.
	 * 
	 * Code modified and used from 
	 * http://stackoverflow.com/questions/1439779/how-to-compare-two-dates-without-the-time-portion
	 * 
	 * @requires c1 != null
	 * @requires c2 != null
	 * @ensures Integer
	 * 
	 */
	public static final Comparator<Calendar> TimeIgnoringComparator = new Comparator<Calendar>() {
		  public int compare(Calendar c1, Calendar c2) {
		    if (c1.get(Calendar.YEAR) != c2.get(Calendar.YEAR)) 
		        return c1.get(Calendar.YEAR) - c2.get(Calendar.YEAR);
		    if (c1.get(Calendar.MONTH) != c2.get(Calendar.MONTH)) 
		        return c1.get(Calendar.MONTH) - c2.get(Calendar.MONTH);
		    return c1.get(Calendar.DAY_OF_MONTH) - c2.get(Calendar.DAY_OF_MONTH);
		  }
	};
}
